using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Xsl;

using Saxon.Api;

using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;


namespace Hotrod.XmlUtility
{
   public class XmlUtility
   {
      // Our static cache manager for managing cached items.
      static CacheManager _cache;

      // Disk location - we expect to find our Enterprise Library configuration and
      // XSL files relative to this path.
      static string _workingPath = null;

      // This can be reused (across threads)
      static Saxon.Api.Processor _processor = new Processor();

      static XmlUtility()
      {
         try
         {
            // We need to query the SSO or some other configuration store to determine where our
            // application is installed (on a server) or "lives" on a developer workstation.
            //
            // Replace this line with whatever configuration mechanism you use.
            //
            _workingPath = SSOSettingsFileReader.ReadString("BizTalkXsltDemo", "WorkingPath");
         }
         catch
         {
         }

         if (_workingPath == null)
         {
            // If the query above doesn't return anything, it could be because our solution isn't
            // deployed.  To faciliate unit testing, we'd like to have a fallback mechanism.
            // If you're using VSTS unit testing, your deployment settings might allow for the 
            // working path to simply be the directory where this assembly resides.
            _workingPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).Substring(6);
         }

         FileConfigurationSource fileSource = new FileConfigurationSource(
            Path.Combine(_workingPath, "EntLib.config"));

         CacheManagerFactory factory = new CacheManagerFactory(fileSource);
         _cache = factory.CreateDefault();
      }

      /// <summary>
      /// Applies an xsl transform and returns the result.
      /// </summary>
      /// <param name="toTransform">An XmlDocument instance that should act as the transform source.</param>
      /// <param name="xslFileName">A bare file name (no path) for the XSLT.  Must live in the "Xslt" subdirectory.</param>
      /// <returns></returns>
      public static XmlDocument ApplyTransform(
         XmlDocument toTransform,
         string xslFileName)
      {
         // Attempt retrieval of compiled xslt from the cache.
         XslCompiledTransform xslt = null;
         xslt = (XslCompiledTransform)_cache.GetData(xslFileName);

         if (xslt == null)
         {
            lock (_cache)
            {
               xslt = (XslCompiledTransform)_cache.GetData(xslFileName);
               if (xslt == null)
               {
                  // If not cached, load it up and cache it.
                  string fullSpec = Path.Combine(Path.Combine(_workingPath,"xslt"), xslFileName);
                  XmlDocument styleSheet = new XmlDocument();
                  styleSheet.Load(fullSpec);

                  // Construct an XsltSettings to allow for the Xsl document() function
                  // as well as for embedded script blocks.
                  XsltSettings settings = new XsltSettings(true, true);

                  // Specify XmlUrlResolver() to enable Xslt import and include elements.
                  xslt = new XslCompiledTransform();
                  xslt.Load(styleSheet, settings, new XmlUrlResolver());

                  // Cache the compiled xslt, ensuring cache flush via FileDependency
                  // if underlying xslt file changes.
                  _cache.Add(
                     xslFileName, 
                     xslt, 
                     CacheItemPriority.Normal,
                     null, 
                     new FileDependency(fullSpec));
               }
            }
         }

         // We want to supply our input to the transform as a node reader to allow for 
         // Xslt operations like xsl:strip-space (which require a streaming input)
         XmlReader readerToTransform = new XmlNodeReader(toTransform);
         XmlDocument transformedDoc = new XmlDocument();
         using (XmlWriter writer = transformedDoc.CreateNavigator().AppendChild())
         {
            xslt.Transform(readerToTransform, (XsltArgumentList)null, writer);
         }

         return transformedDoc;
      }


      /// <summary>
      /// Applies an xsl transform using the SaxonB library and returns the result.
      /// Enables use of XSLT 2.0 and Saxon's extension library, among other things.
      /// </summary>
      /// <param name="toTransform">An XmlDocument instance that should act as the transform source.</param>
      /// <param name="xslFileName">A bare file name (no path) for the XSLT.  Must live in the "Xslt" subdirectory.</param>
      /// <returns></returns>
      public static XmlDocument ApplyTransformWithSaxonB(
         XmlDocument toTransform,
         string xslFileName)
      {
         string cacheKey = xslFileName + "sax";

         // Attempt retrieval of compiled xslt from the cache.  Appending "sax" to avoid
         // key collision in the rare case where same stylesheet is used with ApplyTransform.
         XsltTransformer xslt = null;
         xslt = (XsltTransformer)_cache.GetData(cacheKey);

         if (xslt == null)
         {
            lock (_cache)
            {
               xslt = (XsltTransformer)_cache.GetData(cacheKey);
               if (xslt == null)
               {
                  // If not cached, load it up and cache it.
                  string fullSpec = Path.Combine(Path.Combine(_workingPath, "xslt"), xslFileName);
                  XmlDocument styleSheet = new XmlDocument();
                  styleSheet.Load(fullSpec);

                  // Create a compiler
                  XsltCompiler compiler = _processor.NewXsltCompiler();

                  // Compile the stylesheet
                  xslt = compiler.Compile(new XmlNodeReader(styleSheet)).Load();

                  // Cache the compiled xslt, ensuring cache flush via FileDependency
                  // if underlying xslt file changes.
                  _cache.Add(
                     cacheKey,
                     xslt,
                     CacheItemPriority.Normal,
                     null,
                     new FileDependency(fullSpec));
               }
            }
         }

         // Run the compiled transformation.
         XdmNode input = _processor.NewDocumentBuilder().Build(toTransform.DocumentElement);
         xslt.InitialContextNode = input;
         DomDestination result = new DomDestination();
         xslt.Run(result);

         return result.XmlDocument;
      }

      /// <summary>
      /// Used to create a composite document, rooted with a root element named "root", from several
      /// input documents.  
      /// </summary>
      /// <param name="docs">Variable length argument list of XmlDocuments.</param>
      /// <returns></returns>
      public static XmlDocument CreateCompositeDocument(
         params XmlDocument[] docs)
      {
         XmlDocument returnDoc = new XmlDocument();
         returnDoc.LoadXml("<root/>");

         for (int i = 0; i < docs.GetLength(0); i++)
         {
            if (docs[i] != null)
            {
               XmlNode node = docs[i].FirstChild;
               // Get past leading PI, comments, etc.
               while (node.NodeType != XmlNodeType.Element)
                  node = node.NextSibling;

               returnDoc.FirstChild.AppendChild(returnDoc.ImportNode(node, true));
            }
         }

         return returnDoc;
      }

   }

   // Called from our sample xslt - not a formal element of this library.
   public class XsltScriptHelper
   {
      public static string AddFiveDays(string date)
      {
         DateTime dateTime = DateTime.Parse(date);
         DateTime next = dateTime + new TimeSpan(5, 0, 0, 0);
         return next.ToString();
      }
   }
}


/// <summary>
/// Helper class to assist with getting readable tracing output.  
/// Don't call this in production code, of course 
/// (or use a if(logger.IsDebugEnabled)type of construct before calling)
/// This sits outside of namespace for brevity within BizTalk expression editor.
/// </summary>
public class Pretty
{
   public static string Print(XmlDocument document)
   {
      try
      {
         StringWriter sw = new StringWriter();
         XmlTextWriter writer = new XmlTextWriter(sw);
         writer.Formatting = Formatting.Indented;
         document.WriteTo(writer);
         writer.Flush();
         return sw.ToString();
      }
      catch
      {
         return document.OuterXml;
      }
   }
}